/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.iotdb.tsfile.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.List;

/**
 * Utils to read/write stream.
 */
public class ReadWriteForEncodingUtils {
    private static final String TOO_LONG_BYTE_FORMAT =
            "tsfile-common BytesUtils: encountered value (%d) that requires more than 4 bytes";

    private ReadWriteForEncodingUtils() {
    }

    /**
     * check all number in a int list and find max bit width.
     *
     * @param list input list
     * @return max bit width
     */
    public static int getIntMaxBitWidth(List<Integer> list) {
        int max = 1;
        for (int num : list) {
            int bitWidth = 32 - Integer.numberOfLeadingZeros(num);
            max = Math.max(bitWidth, max);
        }
        return max;
    }

    /**
     * check all number in a long list and find max bit width.
     *
     * @param list input list
     * @return max bit width
     */
    public static int getLongMaxBitWidth(List<Long> list) {
        int max = 1;
        for (long num : list) {
            int bitWidth = 64 - Long.numberOfLeadingZeros(num);
            max = Math.max(bitWidth, max);
        }
        return max;
    }

    /**
     * transform an int var to byte[] format.
     */
    public static byte[] getUnsignedVarInt(int value) {
        int preValue = value;
        int length = 0;
        while ((value & 0xFFFFFF80) != 0L) {
            length++;
            value >>>= 7;
        }
        length++;

        byte[] res = new byte[length];
        value = preValue;
        int i = 0;
        while ((value & 0xFFFFFF80) != 0L) {
            res[i] = (byte) ((value & 0x7F) | 0x80);
            value >>>= 7;
            i++;
        }
        res[i] = (byte) (value & 0x7F);
        return res;
    }

    /**
     * read an unsigned var int in stream and transform it to int format.
     *
     * @param in stream to read an unsigned var int
     * @return integer value
     * @throws IOException exception in IO
     */
    public static int readUnsignedVarInt(InputStream in) throws IOException {
        int value = 0;
        int i = 0;
        int b = in.read();
        while (b != -1 && (b & 0x80) != 0) {
            value |= (b & 0x7F) << i;
            i += 7;
            b = in.read();
        }
        return value | (b << i);
    }

    public static int readVarInt(InputStream in) throws IOException {
        int value = readUnsignedVarInt(in);
        int x = value >>> 1;
        if ((value & 1) != 0) {
            x = ~x;
        }
        return x;
    }

    /**
     * read an unsigned var int in stream and transform it to int format.
     *
     * @param buffer stream to read an unsigned var int
     * @return integer value
     */
    public static int readUnsignedVarInt(ByteBuffer buffer) {
        int value = 0;
        int i = 0;
        int b = 0;
        while (buffer.hasRemaining() && ((b = buffer.get()) & 0x80) != 0) {
            value |= (b & 0x7F) << i;
            i += 7;
        }
        return value | (b << i);
    }

    public static int readVarInt(ByteBuffer buffer) {
        int value = readUnsignedVarInt(buffer);
        int x = value >>> 1;
        if ((value & 1) != 0) {
            x = ~x;
        }
        return x;
    }

    /**
     * write a value to stream using unsigned var int format. for example, int 123456789 has its
     * binary format 00000111-01011011-11001101-00010101 (if we omit the first 5 0, then it is
     * 111010-1101111-0011010-0010101), function writeUnsignedVarInt will split every seven bits and
     * write them to stream from low bit to high bit like: 1-0010101 1-0011010 1-1101111 0-0111010 1
     * represents has next byte to write, 0 represents number end.
     *
     * @param value value to write into stream
     * @param out   output stream
     * @return the number of bytes that the value consume.
     */
    public static int writeUnsignedVarInt(int value, ByteArrayOutputStream out) {
        int position = 1;
        while ((value & 0xFFFFFF80) != 0L) {
            out.write((value & 0x7F) | 0x80);
            value >>>= 7;
            position++;
        }
        out.write(value & 0x7F);
        return position;
    }

    public static int writeVarInt(int value, ByteArrayOutputStream out) {
        int uValue = value << 1;
        if (value < 0) {
            uValue = ~uValue;
        }
        return writeUnsignedVarInt(uValue, out);
    }

    public static int writeUnsignedVarInt(int value, OutputStream out) throws IOException {
        int position = 1;
        while ((value & 0xFFFFFF80) != 0L) {
            out.write((value & 0x7F) | 0x80);
            value >>>= 7;
            position++;
        }
        out.write(value & 0x7F);
        return position;
    }

    public static int writeVarInt(int value, OutputStream out) throws IOException {
        int uValue = value << 1;
        if (value < 0) {
            uValue = ~uValue;
        }
        return writeUnsignedVarInt(uValue, out);
    }

    /**
     * write a value to stream using unsigned var int format. for example, int 123456789 has its
     * binary format 111010-1101111-0011010-0010101, function writeUnsignedVarInt will split every
     * seven bits and write them to stream from low bit to high bit like: 1-0010101 1-0011010
     * 1-1101111 0-0111010 1 represents has next byte to write, 0 represents number end.
     *
     * @param value  value to write into stream
     * @param buffer where to store the result. buffer.remaining() needs to >= 32. Notice: (1) this
     *               function does not check buffer's remaining(). (2) the position will be updated.
     * @return the number of bytes that the value consume.
     * @throws IOException exception in IO
     */
    public static int writeUnsignedVarInt(int value, ByteBuffer buffer) {
        int position = 1;
        while ((value & 0xFFFFFF80) != 0L) {
            buffer.put((byte) ((value & 0x7F) | 0x80));
            value >>>= 7;
            position++;
        }
        buffer.put((byte) (value & 0x7F));
        return position;
    }

    public static int writeVarInt(int value, ByteBuffer buffer) {
        int uValue = value << 1;
        if (value < 0) {
            uValue = ~uValue;
        }
        return writeUnsignedVarInt(uValue, buffer);
    }

    /**
     * Returns the encoding size in bytes of its input value.
     *
     * @param value the integer to be measured
     * @return the encoding size in bytes of its input value
     */
    public static int varIntSize(int value) {
        int uValue = value << 1;
        if (value < 0) {
            uValue = ~uValue;
        }
        int position = 1;
        while ((uValue & 0xFFFFFF80) != 0L) {
            uValue >>>= 7;
            position++;
        }
        return position;
    }

    /**
     * Returns the encoding size in bytes of its input value.
     *
     * @param value the unsigned integer to be measured
     * @return the encoding size in bytes of its input value
     */
    public static int uVarIntSize(int value) {
        int position = 1;
        while ((value & 0xFFFFFF80) != 0L) {
            value >>>= 7;
            position++;
        }
        return position;
    }

    /**
     * write integer value using special bit to output stream.
     *
     * @param value    value to write to stream
     * @param out      output stream
     * @param bitWidth bit length
     * @throws IOException exception in IO
     */
    public static void writeIntLittleEndianPaddedOnBitWidth(int value, OutputStream out, int bitWidth)
            throws IOException {
        int paddedByteNum = (bitWidth + 7) / 8;
        if (paddedByteNum > 4) {
            throw new IOException(String.format(TOO_LONG_BYTE_FORMAT, paddedByteNum));
        }
        int offset = 0;
        while (paddedByteNum > 0) {
            out.write((value >>> offset) & 0xFF);
            offset += 8;
            paddedByteNum--;
        }
    }

    /**
     * write long value using special bit to output stream.
     *
     * @param value    value to write to stream
     * @param out      output stream
     * @param bitWidth bit length
     * @throws IOException exception in IO
     */
    public static void writeLongLittleEndianPaddedOnBitWidth(
            long value, OutputStream out, int bitWidth) throws IOException {
        int paddedByteNum = (bitWidth + 7) / 8;
        if (paddedByteNum > 8) {
            throw new IOException(String.format(TOO_LONG_BYTE_FORMAT, paddedByteNum));
        }
        out.write(BytesUtils.longToBytes(value, paddedByteNum));
    }

    /**
     * read integer value using special bit from input stream.
     *
     * @param buffer   byte buffer
     * @param bitWidth bit length
     * @return integer value
     * @throws IOException exception in IO
     */
    public static int readIntLittleEndianPaddedOnBitWidth(ByteBuffer buffer, int bitWidth)
            throws IOException {
        int paddedByteNum = (bitWidth + 7) / 8;
        if (paddedByteNum > 4) {
            throw new IOException(String.format(TOO_LONG_BYTE_FORMAT, paddedByteNum));
        }
        int result = 0;
        int offset = 0;
        while (paddedByteNum > 0) {
            int ch = ReadWriteIOUtils.read(buffer);
            result += ch << offset;
            offset += 8;
            paddedByteNum--;
        }
        return result;
    }

    /**
     * read long value using special bit from input stream.
     *
     * @param buffer   byte buffer
     * @param bitWidth bit length
     * @return long long value
     * @throws IOException exception in IO
     */
    public static long readLongLittleEndianPaddedOnBitWidth(ByteBuffer buffer, int bitWidth)
            throws IOException {
        int paddedByteNum = (bitWidth + 7) / 8;
        if (paddedByteNum > 8) {
            throw new IOException(String.format(TOO_LONG_BYTE_FORMAT, paddedByteNum));
        }
        long result = 0;
        for (int i = 0; i < paddedByteNum; i++) {
            int ch = ReadWriteIOUtils.read(buffer);
            result <<= 8;
            result |= (ch & 0xff);
        }
        return result;
    }
}
